﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.SessionState;
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>Checks whether a page or URL match filter criteria.</summary>
    /// <remarks><see cref="RequestFilter.Default"/> settings can be configured in an application config file.</remarks>
    public sealed class RequestFilter
    {
        #region Fields.

        string[] httpMethods;
        bool asyncPostBacksLogged;
        Regex relativePathFilter;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of <b>RequestFilter</b> class.
        /// </summary>
        public RequestFilter()
            : this(new string[] { "GET", "POST" }, false, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>RequestFilter</b> class.
        /// </summary>
        /// <param name="httpMethods">HTTP methods/request types which are traced.</param>
        /// <param name="asyncPostBacksLogged">Whether AJAX asynchonous post-backs are traced.</param>
        /// <param name="relativePathFilter">Which pages, as specified by <see cref="HttpRequest.AppRelativeCurrentExecutionFilePath"/> property, are traced.</param>
        public RequestFilter(string[] httpMethods, bool asyncPostBacksLogged, Regex relativePathFilter)
        {
            this.HttpMethods = httpMethods;
            this.AsyncPostBacksLogged = asyncPostBacksLogged;
            this.RelativePathFilter = relativePathFilter;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets or sets HTTP methods/request types which are traced.
        /// </summary>
        /// <value>An array of HTTP method names. By default <b>GET</b> and <b>POST</b> methods are traced.</value>
        public string[] HttpMethods
        {
            get { return this.httpMethods; }
            set { this.httpMethods = value ?? new string[0]; }
        }

        /// <summary>
        /// Gets or sets whether AJAX asynchonous post-backs are traced.
        /// </summary>
        /// <value><b>true</b> if aync post-backs are traced; <b>false</b> otherwise. Default is <b>false</b>.</value>
        public bool AsyncPostBacksLogged
        {
            get { return this.asyncPostBacksLogged; }
            set { this.asyncPostBacksLogged = value; }
        }

        /// <summary>
        /// Gets or sets a regular expression which specifies which pages, as specified by <see cref="HttpRequest.AppRelativeCurrentExecutionFilePath"/> property, are traced.
        /// </summary>
        /// <value>Regular expression pattern or <b>null</b> if all pages are traced. Default is <b>null</b>.</value>
        /// <remarks>
        /// <example>The following example shows how to trace all except the following pages: <b>Default.aspx</b>, 
        /// <b>Toolbar.aspx</b>, <b>MenuFrm.aspx</b>, <b>SearchFrm.aspx</b>, <b>SearchLightFrm.aspx</b>. The solution 
        /// is to use a regular expression pattern which uses <b>negative lookahead</b> element.
        /// <code>
        /// RequestFilter pagesToTrace = new RequestFilter();
        /// pagesToTrace.RelativePathFilter = new Regex("^~/(?!(Default|Toolbar|MenuFrm|SearchFrm|SearchLightFrm).aspx).*$", RegexOptions.Compiled);
        /// </code>
        /// The pattern is pronounced like this: "A string begins with tilde-slash which are not followed by Default.aspx, 
        /// Toolbar.aspx etc. but by any other characters."
        /// </example>
        /// </remarks>
        public Regex RelativePathFilter
        {
            get { return this.relativePathFilter; }
            set { this.relativePathFilter = value; }
        }

        #endregion

        #region Default configuration.

        static RequestFilter defaultConfiguration;

        /// <summary>
        /// Gets a clone of default RequestFilter as configured in an application's config file.
        /// </summary>
        /// <value>An instance (singleton) of RequestFilter class.</value>
        /// <remarks>Default configuration is configured in the <b>appSettings</b> section of config file. Example:
        /// <code>
        /// <add key="BizElements.Web.RequestFilter.HttpMethods" value="GET;POST"/>
        /// <add key="BizElements.Web.RequestFilter.AsyncPostBacksLogged" value="false"/>
        /// <add key="BizElements.Web.RequestFilter.RelativePathFilter" value="^~/(?!(Default|Toolbar|MenuFrm|SearchFrm|SearchLightFrm).aspx).*$"/>
        /// </code>
        /// </remarks>
        public static RequestFilter Default
        {
            get
            {
                if (defaultConfiguration == null)
                {
                    defaultConfiguration = new RequestFilter();
                    string cfgHttpMethods = ConfigurationManager.AppSettings["BizElements.Web.RequestFilter.HttpMethods"];
                    if (cfgHttpMethods != null)
                        defaultConfiguration.HttpMethods = cfgHttpMethods.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

                    string cfgAsyncPostBacksLogged = ConfigurationManager.AppSettings["BizElements.Web.RequestFilter.AsyncPostBacksLogged"];
                    if (cfgAsyncPostBacksLogged == "true")
                        defaultConfiguration.AsyncPostBacksLogged = true;

                    string cfgRelativePathFilter = ConfigurationManager.AppSettings["BizElements.Web.RequestFilter.RelativePathFilter"];
                    if (cfgRelativePathFilter != null)
                        defaultConfiguration.RelativePathFilter = new Regex(cfgRelativePathFilter, RegexOptions.Compiled);
                }

                return (RequestFilter)defaultConfiguration.MemberwiseClone();
            }
        }

        #endregion

        #region Methods.

        /// <summary>
        /// Checks whether the provided page matches the filter and should be traced.
        /// </summary>
        /// <param name="page">Page to check.</param>
        /// <returns><b>true</b> if the page matches the current filter; <b>false</b> othewise.</returns>
        public bool MatchesFilter(Page page)
        {
            HttpRequest req = page.Request;
            bool methodIsMatch = ArrayUtil.Contains<string>(this.HttpMethods, req.HttpMethod);
            if (!methodIsMatch)
                return false;

            if (!this.AsyncPostBacksLogged)
            {
                ScriptManager atlasManager = ScriptManager.GetCurrent(page);
                bool isAsyncPostBack = (atlasManager != null) && atlasManager.IsInAsyncPostBack;
                if (isAsyncPostBack)
                    return false;
            }

            if (this.RelativePathFilter != null)
            {
                bool pathIsMatch = this.RelativePathFilter.IsMatch(page.Request.AppRelativeCurrentExecutionFilePath);
                if (!pathIsMatch)
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Checks whether the provided request matches the filter and should be traced.
        /// </summary>
        /// <param name="request">Request to check.</param>
        /// <returns><b>true</b> if the request matches the current filter; <b>false</b> othewise.</returns>
        public bool MatchesFilter(HttpRequest request)
        {
            bool methodIsMatch = ArrayUtil.Contains<string>(this.HttpMethods, request.HttpMethod);
            if (!methodIsMatch)
                return false;

            if (this.RelativePathFilter != null)
            {
                bool pathIsMatch = this.RelativePathFilter.IsMatch(request.AppRelativeCurrentExecutionFilePath);
                if (!pathIsMatch)
                    return false;
            }

            return true;
        }

        #endregion
    }
}